#region

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Linq.Expressions;
using System.Threading;

#endregion

namespace SharpObservation
{
    /// <summary>
    /// Base for implementations of the INotifyPropertyChanging and INotifyPropertyChanged pattern.
    /// </summary>
    /// <remarks>
    /// <para>
    ///   ObservableObject serves as a base class for derived types that implement the property change notiofication pattern.
    ///   In addition to the standard .NET property change notifications, this class provides several useful features:</para>
    /// <list type="bullet">
    /// <item>Centralized property change implementation which bypassing event firings if no listeners are attached.</item>
    /// <item>Ability to undo property changes.</item>
    /// <item>IsNotificationActive property indicates whether the object is being listened to.</item>
    /// </list> 
    /// <para>
    /// A snippet is available (if installed from Snippets.vsi) to create properties, just type: "soprop [tab][tab]".
    /// </para>
    /// <para>The following example shows how to create an observable class with a name property:</para>
    /// <code>
    /// public class SimpleButObservable : ObservableObject
    /// {
    ///      private string _name;
    ///     
    ///      public string Name
    ///      {
    ///          get { return _name; }
    ///          set { SetProperty(nameof(_name), ref _name, value); }
    ///      }
    /// }
    /// </code>
    /// </remarks>
    public abstract class ObservableObject : EventsObject, INotifyPropertyChanging, INotifyPropertyChanged
    {
        #region INotifyPropertyChanging

        /// <summary>
        ///   Event Key for PropertyChanging Event
        /// </summary>
        protected static readonly EventKey<PropertyChangingEventHandler> PropertyChangingEventKey =
                              new EventKey<PropertyChangingEventHandler>();

        /// <summary>
        ///   Notifies listeners that a property has been changed.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged
        {
            // Add the input delegate to the collection.
            add
            {
                AddHandler(PropertyChangedEventKey, value);
            }
            // Remove the input delegate from the collection.
            remove
            {
                RemoveHandler(PropertyChangedEventKey, value);
            }
        }

        /// <summary>
        ///   Sends an "all property" changing notification to all listeners
        /// </summary>
        protected void OnPropertyChanging()
        {
            OnPropertyChanging(new PropertyChangingEventArgs(null));
        }

        /// <summary>
        ///   Sends a property changing notification to all listeners
        /// </summary>
        protected void OnPropertyChanging(PropertyChangingEventArgs args)
        {
            RaiseEvent(PropertyChangingEventKey, evt => evt(this, args));
        }

        /// <summary>
        ///   Sends a property changing notification to all listeners
        /// </summary>
        /// <param name = "propertyName"></param>
        protected void OnPropertyChanging(string propertyName)
        {
            RaiseEvent(PropertyChangingEventKey, evt => evt(this, new PropertyChangingEventArgs(propertyName)));
        }

        #endregion

        #region INotifyPropertyChanged 

        /// <summary>
        ///   Event Key for PropertyChanged Event
        /// </summary>
        protected static readonly EventKey<PropertyChangedEventHandler> PropertyChangedEventKey =
                              new EventKey<PropertyChangedEventHandler>();

        /// <summary>
        ///   Notifies listeners that a property is about to change.
        /// </summary>
        public event PropertyChangingEventHandler PropertyChanging
        {
            // Add the input delegate to the collection.
            add
            {
                AddHandler(PropertyChangingEventKey, value);
            }
            // Remove the input delegate from the collection.
            remove
            {
                RemoveHandler(PropertyChangingEventKey, value);
            }
        }

        /// <summary>
        ///   Sends an "all property" changed notification to all listeners
        /// </summary>
        protected void OnPropertyChanged()
        {
            RaiseEvent(PropertyChangedEventKey, propertyChanged => propertyChanged(this, new PropertyChangedEventArgs(null)));
        }

        /// <summary>
        ///   Sends a property changed notification to all listeners
        /// </summary>
        protected virtual void OnPropertyChanged(PropertyChangedEventArgs args)
        {
            RaiseEvent(PropertyChangedEventKey, propertyChanged => propertyChanged(this, args));
        }

        /// <summary>
        ///   Sends a property changed notification to all listeners
        /// </summary>
        /// <param name = "propertyName">The name of the property that was changed</param>
        protected void OnPropertyChanged(string propertyName)
        {
            RaiseEvent(PropertyChangedEventKey, propertyChanged => propertyChanged(this, new PropertyChangedEventArgs(propertyName)));
        }


        #endregion

        #region SetProperty

        /// <summary>
        /// Executes the changing of a property from one Value to another.
        /// </summary>
        /// <typeparam name = "T">The type of the property being set</typeparam>
        /// <param name = "propertyName">The name of the property which is being changed</param>
        /// <param name = "field">The old value of the property</param>
        /// <param name = "value">The (desireD) new value of the property</param>
        protected void SetProperty<T>(
            string propertyName,
            ref T field,
            T value)            
        {
            if (EqualityComparer<T>.Default.Equals(field, value)) return;

            OnPropertyChanging(propertyName);
            field = value;
            OnPropertyChanged(propertyName);
        }

        #endregion
    }
}